Scalatra2.2+scalatra-swaggerでREST APIのリファレンスを生成する #2
REST APIの作成
前回の続きです。まずは、適当なREST APIを実装します。
SwaggerTestController.scala
package jp.classmethod.scalatraswagger import org.scalatra._ import org.scalatra.json._ import org.json4s._ class SwaggerTestController extends ScalatraServlet with JacksonJsonSupport { protected implicit val jsonFormats: Formats = DefaultFormats private[this] val postRepo = new PostRepository before() { contentType = formats("json") } get("/") { params.getAs[String]("name") match { case Some(name) => postRepo getPostsByName name case None => postRepo.allPosts } } post("/") { parsedBody.extractOpt[Post] match { case Some(post) => postRepo addPost post Ok() case _ => halt(400, "invalid params") } } notFound { halt(404) } }
Post.scala
package jp.classmethod.scalatraswagger case class Post(name: String, comment: String) class PostRepository { private[this] var posts: IndexedSeq[Post] = IndexedSeq.empty def addPost(post: Post) { posts +:= post } def allPosts = posts def getPostsByName(name: String) = { posts filter { _.name == name } } }
名前とコメントからなる投稿の、一覧の取得と追加機能をAPIとして提供しています。GETとPOSTだけの簡単なサンプルです。
コントローラを作成したら、Scalatraのブートストラップに登録します。
Scalatra.scala
import jp.classmethod.scalatraswagger._ import org.scalatra._ import javax.servlet.ServletContext /** * This is the Scalatra bootstrap file. You can use it to mount servlets or * filters. It's also a good place to put initialization code which needs to * run at application start (e.g. database configurations), and init params. */ class Scalatra extends LifeCycle { override def init(context: ServletContext) { // REST APIを叩くサンプルクライアントページを返すコントローラをマウント context.mount(new RootController, "/*") // REST APIのルーティングを行うコントローラをマウント context.mount(new SwaggerTestController, "/posts/*") } }
これでサンプルREST APIは完成です。適当に作ったクライアントから叩いてきちんと動作するか確認しておきます。以下のスクリーンショットは、クライアントからAPIを叩いて数回POSTした結果をGETした際の様子です。
Swagger specを提供するコントローラを作成する
コントローラを作成
では、準備が整ったので、APIのコントローラをSwaggerに対応させます。まずは、Swagger specをクライアントに提供するコントローラを作成します。
ResourcesApp.scala
package jp.classmethod.scalatraswagger import org.scalatra.swagger.{JacksonSwaggerBase, Swagger, SwaggerBase} import org.scalatra.ScalatraServlet import com.wordnik.swagger.core.SwaggerSpec class ResourcesApp(implicit val swagger: Swagger) extends ScalatraServlet with JacksonSwaggerBase class SwaggerTestSwagger extends Swagger(SwaggerSpec.version, "1.0")
このコントローラは通常のコントローラ同様、ScalatraServlet抽象クラスを実装したサーブレットとして作成します。このサーブレットにJacksonSwaggerBaseトレイトをミックスインすればSwagger specを提供するコントローラの出来上がりです。
また、コンストラクタではSwagger型のオブジェクトを暗黙のパラメータとして取るよう実装されています。これは、JacksonSwaggerBaseのスーパートレイトであるSwaggerBaseトレイトで、Swagger型の抽象メソッドswaggerが定義されているためです。Swagger型のオブジェクトは、APIリファレンスに記述される情報を管理するオブジェクトです。JacksonSwaggerBaseトレイトではこのメソッドが実装されていないため、ResourceAppでswaggerを実装しています。
9行目では、このResourceAppに渡すSwagger型のクラスとして、Swaggerクラスのサブクラスを実装しています。コンストラクタで渡しているのは、Swagger specのバージョンとREST APIのバージョンを表すStringです。
コントローラをブートストラップに登録
先ほど作成したコントローラをScalatraのブートストラップに登録します。
Scalatra.scala
import jp.classmethod.scalatraswagger._ import org.scalatra._ import javax.servlet.ServletContext /** * This is the Scalatra bootstrap file. You can use it to mount servlets or * filters. It's also a good place to put initialization code which needs to * run at application start (e.g. database configurations), and init params. */ class Scalatra extends LifeCycle { implicit val swagger = new SwaggerTestSwagger override def init(context: ServletContext) { // REST APIを叩くサンプルクライアントページを提供するコントローラをマウント context.mount(new RootController, "/*") // REST APIのルーティングを行うコントローラをマウント context.mount(new SwaggerTestController, "/posts/*") // SwaggerのJSON specを提供するコントローラをマウント context.mount(new ResourcesApp, "/api-docs/*") } }
20行目でResourceAppを"api-docs"というurlでアクセスできるように設定しています。また12行目では、ResourceAppのコンストラクタに暗黙のパラメータとして渡すSwaggerTestSwagger型のインスタンスを生成しています。
Swagger specを取得してみる
では、ブラウザでhttp://localhost:8080/api-docs/resources.jsonにアクセスしてみます。すると、以下のようなJSON形式のSwagger specが返されます。
{"basePath":"http://localhost:8080","swaggerVersion":"1.1","apiVersion":"1.0","apis":[]}
今の段階ではまだリソースの情報が記述されていません。
リファレンスの内容をREST APIのコントローラに記述する
Swagger specが取得できるようになりましたので、リソースの各操作の情報をREST APIのコントローラに記述していきます。
SwaggerSupportトレイトをミックスイン
コントローラにSwagger specの内容を記述するために、SwaggerSupportトレイトをミックスインします。
SwaggerTestController.scala
import org.scalatra.swagger._ ... class SwaggerTestController(implicit val swagger: Swagger) extends ScalatraServlet with JacksonJsonSupport with SwaggerSupport { ... }
このコントローラも、Scalatraのブートストラップで暗黙のパラメータとして宣言されたSwaggerTestSwaggerのインスタンスをコンストラクタで受け取ります。
APIの概要を記述
コントローラに、APIの概要としてAPIの名前とその説明を記述します。
SwaggerTestController.scala
class SwaggerTestController(implicit val swagger: Swagger) extends ScalatraServlet with JacksonJsonSupport with SwaggerSupport { ... // Swagger specに出力するAPIの名前 override protected val applicationName = Some("posts") // Swagger specに出力するAPIの概要 protected val applicationDescription = "コメントの投稿と、投稿されたコメントの取得機能を提供するAPIです。" ... }
applicationNameにAPIの名前を、applicationDescriptionにAPIの概要を記述します。この2つの変数はSwaggerSupportトレイトのスーパートレイトであるSwaggerSupportSyntaxトレイトでメソッドとして定義されているものを、それぞれオーバーライド・実装しています。
アクションに記述
最後に、コントローラの各アクションに提供するAPIの情報を定義します。
SwaggerTestController.scala
get("/", operation( apiOperation[List[Post]]("getPosts") summary "全ての投稿を取得" notes "全ての投稿を取得します。名前による絞り込みもできます。" parameter queryParam[String]("name").description("取得する投稿の名前").optional )) { params.getAs[String]("name") match { case Some(name) => postRepo getPostsByName name case None => postRepo.allPosts } } post("/", operation( apiOperation[Unit]("addPost") summary "コメントを投稿" notes """|コメントを投稿します。必ず名前(name)とコメント(comment)をJSON形式のパラメータとして渡す必要があります。 |<br>どちらかが渡されなかった場合、400を返します。""".stripMargin parameter bodyParam[Post]("post").description("投稿データ").required error Error(400, "パラメータが不正") )) { parsedBody.extractOpt[Post] match { case Some(post) => postRepo addPost post Ok() case _ => halt(400, "invalid params") } }
Scalatraのgetメソッドの最初の引数リストは元々RouteTransformer型の可変長引数になっています。scalatra-swaggerではこれを利用してリファレンスに記述する情報のメタデータを定義しています。
OperationBuilder
apiOperationメソッドはOperationBuilder型を返すメソッドです。型パラメータで戻り値の型を、引数でnicknameを指定します。nicknameは、Swaggerがメソッドを識別するために利用するキーで、主にクライアントでのメソッド生成に利用されています。
OperationBuilderは各オペレーションの情報を設定するためのビルダで、このOperationBuilderの各種メソッドを利用して情報を記述していきます。OperationBuilderのメソッドは戻り値として自身を返すので、メソッドチェーンを組むことができます。上記コードの様に、メソッドチェーンを中置記法で書くと内容が見やすくなります。
OperationBuilderのメソッドは、SwaggerOperationBuilderトレイトで実装されており、以下のメソッドが利用できます。
- summary
- 対象のオペレーションで行う処理の要約を簡潔に記述します。
- notes
- 対象のオペレーションで行う処理の要約を記述します。
- parameter/parameters
- このオペレーションで受け取ることのできるパラメータを指定します。パラメータ1つにつき、Parameter型のインスタンス1つを引数に渡します。上記コードではクエリストリングでパラメータを渡すので、queryParamメソッドでParameter型のインスタンスを生成し、型パラメータとしてクエリストリングのパラメータの型であるString型を渡しています。さらに、descriptionメソッドでパラメータの説明を設定し、optionalメソッドで任意のパラメータであるとをマークしています。
- deprecated
- オペレーションを非推奨としてマークします。
- error/errors
- 返されるエラーコードとその理由を記述します。org.scalatra.swagger.Error型のケースクラスを引数に渡します。
以上で、各オペレーションの情報の記述は完了です。これで、Swagger specが生成されるようになりました。
Swagger specを取得してみる
では、再度ブラウザでhttp://localhost:8080/api-docs/resources.jsonにアクセスしてみます。すると、以下のようなJSON形式のSwagger specが返されます。
{"basePath":"http://localhost:8080","swaggerVersion":"1.1","apiVersion":"1.0","apis":[{"path":"/api-docs/posts.{format}","description":"コメントの投稿と、投稿されたコメントの取得機能を提供するAPIです。"}]}
apisキーの内容が記述されており、postsリソースの詳細情報を取得するパスなどが定義されています。
生成されたリファレンスを確認する
では、生成されたSwagger specをクライアントで利用してリファレンスを表示させてみましょう。クライアントは、swagger-uiのGitHubから手に入れてもいいですが、Swaggerのデモサイトを利用するのが一番手軽です。デモサイトを開いたら、サイト上部の中央にあるURL入力欄に、http://localhost:8080/api-docs/resources.jsonと入力してEnterキーを押下します。
すると、Swagger specがロードされて以下のようなページが表示されます。
ビューを展開して詳細情報を表示しすると下のような感じです。
なお、Swaggerのデモサイトのクライアントがlocalhostにアクセスできているのは、ScalatraのCorsSupportトレイトがCross-Origin Resource Sharingをデフォルトで全てのドメインに対して許可するよう設定しているためです。CorsSupportトレイトは、SwaggerTestControllerでミックスインしているSwaggerSupportトレイトによってミックスインされています。この動作を変更する場合はCorsSupportトレイトの動作を修正する必要があります。
まとめ
scalatra-swaggerのおかげで、とても簡単にSwagger specを生成することができました。また、各アクションに定義したオペレーションの情報は、ソースコードのコメントの代わりにもなるので、常にソースコードとドキュメントの内容を一致させることがとても楽にできそうです。最初にSwaggerの使い方を覚える必要がありますが、プロジェクトによってはそのコストに見合う効果が得られそうだと思いました。
ソースコードはGitHubに公開しています。